package com.platform.boot.security;

import com.platform.boot.annotation.RestServerException;
import com.platform.boot.security.group.authority.GroupAuthoritiesService;
import com.platform.boot.security.group.authority.GroupAuthorityRequest;
import com.platform.boot.security.group.member.GroupMemberOnly;
import com.platform.boot.security.group.member.GroupMemberRequest;
import com.platform.boot.security.group.member.GroupMembersService;
import com.platform.boot.security.tenant.member.TenantMemberOnly;
import com.platform.boot.security.tenant.member.TenantMemberRequest;
import com.platform.boot.security.tenant.member.TenantMembersService;
import com.platform.boot.security.user.User;
import com.platform.boot.security.user.UsersService;
import com.platform.boot.security.user.authority.UserAuthoritiesService;
import com.platform.boot.security.user.authority.UserAuthorityRequest;
import com.platform.boot.utils.BaseAutoToolsUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.query.Query;
import org.springframework.data.relational.core.query.Update;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.time.LocalDateTime;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * @author <a href="https://github.com/vnobo">Alex bob</a>
 */
@Service
@RequiredArgsConstructor
public class SecurityUserManager extends BaseAutoToolsUtil implements ReactiveUserDetailsService {
    private final UsersService usersService;
    private final UserAuthoritiesService userAuthoritiesService;
    private final GroupAuthoritiesService groupAuthoritiesService;
    private final GroupMembersService groupMembersService;
    private final TenantMembersService tenantMembersService;

    @Override
    public Mono<UserDetails> findByUsername(String username) {
        Function<User, Flux<GrantedAuthority>> authoritiesFunction = user ->
                this.authorities(Optional.ofNullable(user.getCode())
                        .orElseThrow(() -> RestServerException.withMsg("User [" + username + "] no code!")));
        return usersService.loadByUsername(username)
                .flatMap(user -> authoritiesFunction.apply(user).collectList().map(authorities ->
                        SecurityUserDetails.of(user.getCode(), username, user.getPassword(), user.getEnabled())
                                .authorities(authorities.parallelStream().collect(Collectors.toSet()))))
                .onErrorResume(throwable -> Mono.defer(() -> Mono.error(new AuthenticationServiceException(
                        throwable.getLocalizedMessage(), throwable))));
    }

    public Mono<SecurityUserDetails> loadSecurity(String username) {

        Mono<SecurityUserDetails> securityDetailsMono = this.findByUsername(username).cast(SecurityUserDetails.class);

        Function<SecurityUserDetails, Flux<GroupMemberOnly>> userGroupFunction = securityDetails ->
                this.groupMembersService.search(GroupMemberRequest.withUserCode(securityDetails.getUserCode()),
                        Pageable.ofSize(Integer.MAX_VALUE)).distinct();

        Function<SecurityUserDetails, Flux<TenantMemberOnly>> userTenantFunction = securityDetails ->
                this.tenantMembersService.search(TenantMemberRequest.withUserCode(securityDetails.getUserCode()),
                        Pageable.ofSize(Integer.MAX_VALUE)).distinct();

        return securityDetailsMono.flatMap(securityDetails -> userTenantFunction.apply(securityDetails)
                        .collectList().map(tenants -> securityDetails.tenants(
                                tenants.parallelStream().collect(Collectors.toSet()))))
                .flatMap(securityDetails -> userGroupFunction.apply(securityDetails)
                        .distinct().collectList().map(groupMemberOnliest -> securityDetails.groups(
                                groupMemberOnliest.parallelStream().collect(Collectors.toSet()))));
    }

    public Mono<Void> loginSuccess(String username) {
        return entityTemplate.update(User.class)
                .matching(Query.query(Criteria.where("username").like(username).ignoreCase(true)))
                .apply(Update.update("loginTime", LocalDateTime.now()))
                .then();
    }

    private Flux<GrantedAuthority> authorities(String userCode) {
        return this.getAuthorities(userCode).concatWith(this.getGroupAuthorities(userCode)).distinct();
    }

    private Flux<GrantedAuthority> getAuthorities(String userCode) {
        return this.userAuthoritiesService.search(UserAuthorityRequest.withUserCode(userCode))
                .cast(GrantedAuthority.class);
    }

    private Flux<GrantedAuthority> getGroupAuthorities(String userCode) {
        return groupMembersService.search(GroupMemberRequest.withUserCode(userCode), Pageable.ofSize(Integer.MAX_VALUE))
                .flatMap(groupMember -> this.groupAuthoritiesService
                        .search(GroupAuthorityRequest.withCode(groupMember.getGroupCode())));
    }


}